/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed under the LGPL, Version 3.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at  http://www.gnu.org/licenses/lgpl-3.0.html
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.minidev.json;

import java.io.IOException;

/**
 * protected class used to stored Internal methods
 *
 * @author Uriel Chemouni &lt;uchemouni@gmail.com&gt;
 */
class JStylerObj {

    public final static MPSimple MP_SIMPLE = new MPSimple();
    public final static MPTrue MP_TRUE = new MPTrue();
    public final static MPAgressive MP_AGGRESIVE = new MPAgressive();

    public final static EscapeLT ESCAPE_LT = new EscapeLT();
    public final static Escape4Web ESCAPE4Web = new Escape4Web();

    public static interface MustProtect {
        public boolean mustBeProtect(String s);
    }

    private static class MPTrue implements MustProtect {
        @Override
        public boolean mustBeProtect(String s) {
            return true;
        }
    }

    private static class MPSimple implements MustProtect {
        /**
         * can a String can be store without enclosing quotes. ie: should not
         * contain any special json char
         *
         * @param s
         * @return
         */
        @Override
        public boolean mustBeProtect(final String s) {
            if (s == null) {
                return false;
            }
            int len = s.length();
            if (len == 0) {
                return true;
            }
            if (s.trim() != s) {
                return true;
            }

            char ch = s.charAt(0);
            if (ch >= '0' && ch <= '9' || ch == '-') {
                return true;
            }

            for (int i = 0; i < len; i++) {
                ch = s.charAt(i);
                if (isSpace(ch)) {
                    return true;
                }
                if (isSpecial(ch)) {
                    return true;
                }
                if (isSpecialChar(ch)) {
                    return true;
                }
                if (isUnicode(ch)) {
                    return true;
                }
            }
            // keyword check
            if (isKeyword(s)) {
                return true;
            }
            return false;
        }
    }

    private static class MPAgressive implements MustProtect {
        @Override
        public boolean mustBeProtect(final String s) {
            if (s == null) {
                return false;
            }
            int len = s.length();
            // protect empty String
            if (len == 0) {
                return true;
            }
            // protect trimable String
            if (s.trim() != s) {
                return true;
            }
            // json special char
            char ch = s.charAt(0);
            if (isSpecial(ch) || isUnicode(ch)) {
                return true;
            }
            for (int i = 1; i < len; i++) {
                ch = s.charAt(i);
                if (isSpecialClose(ch) || isUnicode(ch)) {
                    return true;
                }
            }
            // keyWord must be protect
            if (isKeyword(s)) {
                return true;
            }
            // Digit like text must be protect
            ch = s.charAt(0);
            // only test String if First Ch is a digit
            if (ch >= '0' && ch <= '9' || ch == '-') {
                int p = 1;
                // skip first digits
                for (; p < len; p++) {
                    ch = s.charAt(p);
                    if (ch < '0' || ch > '9') {
                        break;
                    }
                }
                // int/long
                if (p == len) {
                    return true;
                }
                // Floating point
                if (ch == '.') {
                    p++;
                }
                // Skip digits
                for (; p < len; p++) {
                    ch = s.charAt(p);
                    if (ch < '0' || ch > '9') {
                        break;
                    }
                }
                if (p == len) {
                    return true; // can be read as an floating number
                }
                // Double
                if (ch == 'E' || ch == 'e') {
                    p++;
                    if (p == len) {// no power data not a digits
                        return false;
                    }
                    ch = s.charAt(p);
                    if (ch == '+' || ch == '-') {
                        p++;
                        ch = s.charAt(p);
                    }
                }
                if (p == len) { // no power data => not a digit
                    return false;
                }
                for (; p < len; p++) {
                    ch = s.charAt(p);
                    if (ch < '0' || ch > '9') {
                        break;
                    }
                }
                // floating point With power of data.
                if (p == len) {
                    return true;
                }
                return false;
            }
            return false;
        }
    }

    public static boolean isSpace(char c) {
        return (c == '\r' || c == '\n' || c == '\t' || c == ' ');
    }

    public static boolean isSpecialChar(char c) {
        return (c == '\b' || c == '\f' || c == '\n');
    }

    public static boolean isSpecialOpen(char c) {
        return (c == '{' || c == '[' || c == ',' || c == ':');
    }

    public static boolean isSpecialClose(char c) {
        return (c == '}' || c == ']' || c == ',' || c == ':');
    }

    public static boolean isSpecial(char c) {
        return (c == '{' || c == '[' || c == ',' || c == '}' || c == ']' || c == ':' || c == '\'' || c == '"');
    }

    public static boolean isUnicode(char c) {
        // ANSI controle char
        return ((c >= '\u0000' && c <= '\u001F') ||
                // DEL or unicode ctrl
                (c >= '\u007F' && c <= '\u009F') ||
                // '\u00A0' No-breakable space ?
                // En Quad .. more
                (c >= '\u2000' && c <= '\u20FF'));
    }

    public static boolean isKeyword(String s) {
        if (s.length() < 3) {
            return false;
        }
        char c = s.charAt(0);
        if (c == 'n') {
            return s.equals("null");
        }
        if (c == 't') {
            return s.equals("true");
        }
        if (c == 'f') {
            return s.equals("false");
        }
        if (c == 'N') {
            return s.equals("NaN");
        }
        return false;
    }

    public static interface StringProtector {
        public void escape(String s, Appendable out);
    }

    private static class EscapeLT implements StringProtector {
        /**
         * Escape special chars form String except /
         *
         * @param s   - Must not be null.
         * @param out
         */
        @Override
        public void escape(String s, Appendable out) {
            try {
                int len = s.length();
                for (int i = 0; i < len; i++) {
                    char ch = s.charAt(i);
                    switch (ch) {
                        case '"':
                            out.append("\\\"");
                            break;
                        case '\\':
                            out.append("\\\\");
                            break;
                        case '\b':
                            out.append("\\b");
                            break;
                        case '\f':
                            out.append("\\f");
                            break;
                        case '\n':
                            out.append("\\n");
                            break;
                        case '\r':
                            out.append("\\r");
                            break;
                        case '\t':
                            out.append("\\t");
                            break;
                        default:
                            // Reference:
                            // http://www.unicode.org/versions/Unicode5.1.0/
                            if ((ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u007F' && ch <= '\u009F')
                                    || (ch >= '\u2000' && ch <= '\u20FF')) {
                                out.append("\\u");
                                String hex = "0123456789ABCDEF";
                                out.append(hex.charAt(ch >> 12 & 0x000F));
                                out.append(hex.charAt(ch >> 8 & 0x000F));
                                out.append(hex.charAt(ch >> 4 & 0x000F));
                                out.append(hex.charAt(ch >> 0 & 0x000F));
                            } else {
                                out.append(ch);
                            }
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException("Impossible Exeption");
            }
        }
    }

    private static class Escape4Web implements StringProtector {

        /**
         * Escape special chars form String including /
         *
         * @param s  - Must not be null.
         * @param sb
         */
        @Override
        public void escape(String s, Appendable sb) {
            try {
                int len = s.length();
                for (int i = 0; i < len; i++) {
                    char ch = s.charAt(i);
                    switch (ch) {
                        case '"':
                            sb.append("\\\"");
                            break;
                        case '\\':
                            sb.append("\\\\");
                            break;
                        case '\b':
                            sb.append("\\b");
                            break;
                        case '\f':
                            sb.append("\\f");
                            break;
                        case '\n':
                            sb.append("\\n");
                            break;
                        case '\r':
                            sb.append("\\r");
                            break;
                        case '\t':
                            sb.append("\\t");
                            break;
                        case '/':
                            sb.append("\\/");
                            break;
                        default:
                            // Reference:
                            // http://www.unicode.org/versions/Unicode5.1.0/
                            if ((ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u007F' && ch <= '\u009F')
                                    || (ch >= '\u2000' && ch <= '\u20FF')) {
                                sb.append("\\u");
                                String hex = "0123456789ABCDEF";
                                sb.append(hex.charAt(ch >> 12 & 0x0F));
                                sb.append(hex.charAt(ch >> 8 & 0x0F));
                                sb.append(hex.charAt(ch >> 4 & 0x0F));
                                sb.append(hex.charAt(ch >> 0 & 0x0F));
                            } else {
                                sb.append(ch);
                            }
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException("Impossible Error");
            }
        }
    }
}
